Interface Implementation topic

Implementing Java interfaces from Dart

Let's take a simple Java interface like Runnable that has a single void method called run:

// Java
public interface Runnable {
  void run();
}

These are the bindings that JNIgen generates for this interface:

// Dart Bindings - Boilerplate omitted for clarity.
class Runnable extends JObject {
  void run() { /* ... */ }

  factory Runnable.implement($Runnable impl) { /* ... */ }
  static void implementIn(
    JImplementer implementer,
    $Runnable impl,
  ) { /* ... */ }
}

abstract base mixin class $Runnable {
  factory $Runnable({
    required void Function() run,
    bool run$async,
  }) = _$Runnable;

  bool get run$async => false;
  void run();
}

class _$Runnable implements $Runnable {
  _$Runnable({
    required void Function() run,
    this.run$async = false;
  }) : _run = run;

  final void Function() _run;
  final bool run$async;

  void run() {
    return _run();
  }
}

Implementing interfaces inline

Runnable is used a lot to pass a void callback to a function. To simply this workflow, Java 8 introduced lambdas.

// Java
Runnable runnable = () -> System.out.println("hello");

To allow the same flexibility in Dart, $Runnable has a default factory that simply gets each method of the interface as a closure argument. So you would do:

// Dart
final runnable = Runnable.implement($Runnable(run: () => print('hello')));

Reuse the same implementation

The reason JNIgen generates the $Runnable class is to make it easier to reuse the same implementation for multiple instances. This is analogous to actually implementing the interface in Java instead of using the lambdas:

// Java
public class Printer implements Runnable {
  private final String text;

  public Printer(String text) {
    this.text = text;
  }

  @Override
  public void run() {
    System.out.println(text);
  }
}

This way you can create multiple such Runnables like new Printer("hello") and new Printer("world").

You can do the same in Dart by creating a subclass that implements $Runnable:

// Dart
final class Printer with $Runnable {
  final String text;

  Printer(this.text);

  @override
  void run() {
    print(text);
  }
}

And similarly write Runnable.implement(Printer('hello')) and Runnable.implement(Printer('world')), to create multiple Runnables and share common logic.

Implement as a listener

By default, when any of methods of the implemented interface gets called, the caller will block until the callee returns a result.

Void-returning functions don't have to return a result, so we can choose to not block the caller when the method is just a listener. To signal this, pass true to <method name>$async argument when implementing the interface inline:

// Dart
final runnable = Runnable.implement($Runnable(
  run: () => print('hello'),
  run$async: true, // This makes the run method non-blocking.
));

Similarly, when subclassing

// Dart
final class Printer with $Runnable {
  final String text;

  Printer(this.text);

  @override
  void run() {
    print(text);
  }

  @override
  bool get run$async => true; // This makes the run method non-blocking.
}

Implement multiple interfaces

To implement more than one interface, use a JImplementer from package:jni. Closable is another simple Java interface that has a single void close method. Here is how we create an object that implements both Runnable and Closable:

// Dart
final implementer = JImplementer();
Runnable.implementIn(implementer, $Runnable(run: () => print('run')));
Closable.implementIn(implementer, $Closable(close: () => print('close')));
final object = implementer.implement(Runnable.type); // or Closable.type.

As the created object implements both Runnable and Closable, it's also possible to make it a Closable by passing in Closable.type to implementer.implement. Or simply cast it after creation:

// Dart
final closable = object.as(Closable.type);

Libraries

jnigen Java Differences Lifecycle Threading Interface Implementation
This library exports a high level programmatic API to jnigen, the entry point of which is runJniGenTask function, which takes run configuration as a JniGenTask.